<?php

namespace gyh\GyhReadsheet\Worksheet;


use gyh\GyhReadsheet\Calculation\Calculation;
use gyh\GyhReadsheet\Calculation\DateTime;
use gyh\GyhReadsheet\Calculation\Functions;
use gyh\GyhReadsheet\Cell\Coordinate;
use gyh\GyhReadsheet\Exception as GyhReadsheetException;
use gyh\GyhReadsheet\Shared\Date;
use gyh\GyhReadsheet\Shared\PasswordHasher;
use ArrayObject;
use gyh\GyhReadsheet\Cell\Cell;
use gyh\GyhReadsheet\Cell\DataType;
use gyh\GyhReadsheet\Cell\DataValidation;
use gyh\GyhReadsheet\Cell\Hyperlink;
use gyh\GyhReadsheet\Chart\Chart;
use gyh\GyhReadsheet\Collection\Cells;
use gyh\GyhReadsheet\Collection\CellsFactory;
use gyh\GyhReadsheet\Comment;
use gyh\GyhReadsheet\Exception;
use gyh\GyhReadsheet\IComparable;
use gyh\GyhReadsheet\NamedRange;
use gyh\GyhReadsheet\ReferenceHelper;
use gyh\GyhReadsheet\RichText\RichText;
use gyh\GyhReadsheet\Shared;
use gyh\GyhReadsheet\Spreadsheet;
use gyh\GyhReadsheet\Style\Color;
use gyh\GyhReadsheet\Style\Conditional;
use gyh\GyhReadsheet\Style\NumberFormat;
use gyh\GyhReadsheet\Style\Style;


abstract class CellIterator implements \Iterator
{
    protected $worksheet;

    protected $onlyExistingCells = false;

    public function __destruct()
    {
        unset($this->worksheet);
    }

    public function getIterateOnlyExistingCells()
    {
        return $this->onlyExistingCells;
    }

    abstract protected function adjustForExistingOnlyRange();

    public function setIterateOnlyExistingCells($value)
    {
        $this->onlyExistingCells = (bool) $value;

        $this->adjustForExistingOnlyRange();
    }
}


class SheetView
{
    const SHEETVIEW_NORMAL = 'normal';
    const SHEETVIEW_PAGE_LAYOUT = 'pageLayout';
    const SHEETVIEW_PAGE_BREAK_PREVIEW = 'pageBreakPreview';

    private static $sheetViewTypes = [
        self::SHEETVIEW_NORMAL,
        self::SHEETVIEW_PAGE_LAYOUT,
        self::SHEETVIEW_PAGE_BREAK_PREVIEW,
    ];

    private $zoomScale = 100;

    private $zoomScaleNormal = 100;

    private $sheetviewType = self::SHEETVIEW_NORMAL;

    public function __construct()
    {
    }

    public function getZoomScale()
    {
        return $this->zoomScale;
    }

    public function setZoomScale($pValue)
    {
        if (($pValue >= 1) || $pValue === null) {
            $this->zoomScale = $pValue;
        } else {
            throw new GyhReadsheetException('Scale must be greater than or equal to 1.');
        }

        return $this;
    }

    public function getZoomScaleNormal()
    {
        return $this->zoomScaleNormal;
    }

    public function setZoomScaleNormal($pValue)
    {
        if (($pValue >= 1) || $pValue === null) {
            $this->zoomScaleNormal = $pValue;
        } else {
            throw new GyhReadsheetException('Scale must be greater than or equal to 1.');
        }

        return $this;
    }

    public function getView()
    {
        return $this->sheetviewType;
    }

    public function setView($pValue)
    {
        if ($pValue === null) {
            $pValue = self::SHEETVIEW_NORMAL;
        }
        if (in_array($pValue, self::$sheetViewTypes)) {
            $this->sheetviewType = $pValue;
        } else {
            throw new GyhReadsheetException('Invalid sheetview layout type.');
        }

        return $this;
    }

    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if (is_object($value)) {
                $this->$key = clone $value;
            } else {
                $this->$key = $value;
            }
        }
    }
}

class RowIterator implements \Iterator
{
    private $subject;

    private $position = 1;

    private $startRow = 1;

    private $endRow = 1;

    public function __construct(Worksheet $subject, $startRow = 1, $endRow = null)
    {
        $this->subject = $subject;
        $this->resetEnd($endRow);
        $this->resetStart($startRow);
    }

    public function __destruct()
    {
        unset($this->subject);
    }

    public function resetStart($startRow = 1)
    {
        if ($startRow > $this->subject->getHighestRow()) {
            throw new GyhReadsheetException("Start row ({$startRow}) is beyond highest row ({$this->subject->getHighestRow()})");
        }

        $this->startRow = $startRow;
        if ($this->endRow < $this->startRow) {
            $this->endRow = $this->startRow;
        }
        $this->seek($startRow);

        return $this;
    }

    public function resetEnd($endRow = null)
    {
        $this->endRow = ($endRow) ? $endRow : $this->subject->getHighestRow();

        return $this;
    }

    public function seek($row = 1)
    {
        if (($row < $this->startRow) || ($row > $this->endRow)) {
            throw new GyhReadsheetException("Row $row is out of range ({$this->startRow} - {$this->endRow})");
        }
        $this->position = $row;

        return $this;
    }

    public function rewind()
    {
        $this->position = $this->startRow;
    }

    public function current()
    {
        return new Row($this->subject, $this->position);
    }

    public function key()
    {
        return $this->position;
    }

    public function next()
    {
        ++$this->position;
    }

    public function prev()
    {
        --$this->position;
    }

    public function valid()
    {
        return $this->position <= $this->endRow && $this->position >= $this->startRow;
    }
}

class RowCellIterator extends CellIterator
{
    private $currentColumnIndex;

    private $rowIndex = 1;

    private $startColumnIndex = 1;

    private $endColumnIndex = 1;

    public function __construct(Worksheet $worksheet = null, $rowIndex = 1, $startColumn = 'A', $endColumn = null)
    {
        $this->worksheet = $worksheet;
        $this->rowIndex = $rowIndex;
        $this->resetEnd($endColumn);
        $this->resetStart($startColumn);
    }

    public function resetStart($startColumn = 'A')
    {
        $this->startColumnIndex = Coordinate::columnIndexFromString($startColumn);
        $this->adjustForExistingOnlyRange();
        $this->seek(Coordinate::stringFromColumnIndex($this->startColumnIndex));

        return $this;
    }

    public function resetEnd($endColumn = null)
    {
        $endColumn = $endColumn ? $endColumn : $this->worksheet->getHighestColumn();
        $this->endColumnIndex = Coordinate::columnIndexFromString($endColumn);
        $this->adjustForExistingOnlyRange();

        return $this;
    }

    public function seek($column = 'A')
    {
        $column = Coordinate::columnIndexFromString($column);
        if (($column < $this->startColumnIndex) || ($column > $this->endColumnIndex)) {
            throw new GyhReadsheetException("Column $column is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})");
        } elseif ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($column, $this->rowIndex))) {
            throw new GyhReadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist');
        }
        $this->currentColumnIndex = $column;

        return $this;
    }

    public function rewind()
    {
        $this->currentColumnIndex = $this->startColumnIndex;
    }

    public function current()
    {
        return $this->worksheet->getCellByColumnAndRow($this->currentColumnIndex, $this->rowIndex);
    }

    public function key()
    {
        return Coordinate::stringFromColumnIndex($this->currentColumnIndex);
    }

    public function next()
    {
        do {
            ++$this->currentColumnIndex;
        } while (($this->onlyExistingCells) && (!$this->worksheet->cellExistsByColumnAndRow($this->currentColumnIndex, $this->rowIndex)) && ($this->currentColumnIndex <= $this->endColumnIndex));
    }

    public function prev()
    {
        do {
            --$this->currentColumnIndex;
        } while (($this->onlyExistingCells) && (!$this->worksheet->cellExistsByColumnAndRow($this->currentColumnIndex, $this->rowIndex)) && ($this->currentColumnIndex >= $this->startColumnIndex));
    }

    public function valid()
    {
        return $this->currentColumnIndex <= $this->endColumnIndex && $this->currentColumnIndex >= $this->startColumnIndex;
    }

    protected function adjustForExistingOnlyRange()
    {
        if ($this->onlyExistingCells) {
            while ((!$this->worksheet->cellExistsByColumnAndRow($this->startColumnIndex, $this->rowIndex)) && ($this->startColumnIndex <= $this->endColumnIndex)) {
                ++$this->startColumnIndex;
            }
            if ($this->startColumnIndex > $this->endColumnIndex) {
                throw new GyhReadsheetException('No cells exist within the specified range');
            }
            while ((!$this->worksheet->cellExistsByColumnAndRow($this->endColumnIndex, $this->rowIndex)) && ($this->endColumnIndex >= $this->startColumnIndex)) {
                --$this->endColumnIndex;
            }
            if ($this->endColumnIndex < $this->startColumnIndex) {
                throw new GyhReadsheetException('No cells exist within the specified range');
            }
        }
    }
}

class RowDimension extends Dimension
{
    private $rowIndex;

    private $height = -1;

    private $zeroHeight = false;

    public function __construct($pIndex = 0)
    {
        $this->rowIndex = $pIndex;

        parent::__construct(null);
    }

    public function getRowIndex()
    {
        return $this->rowIndex;
    }

    public function setRowIndex($pValue)
    {
        $this->rowIndex = $pValue;

        return $this;
    }

    public function getRowHeight()
    {
        return $this->height;
    }

    public function setRowHeight($pValue)
    {
        $this->height = $pValue;

        return $this;
    }

    public function getZeroHeight()
    {
        return $this->zeroHeight;
    }

    public function setZeroHeight($pValue)
    {
        $this->zeroHeight = $pValue;

        return $this;
    }
}


class Protection
{
    private $sheet = false;

    private $objects = false;

    private $scenarios = false;

    private $formatCells = false;

    private $formatColumns = false;

    private $formatRows = false;

    private $insertColumns = false;

    private $insertRows = false;

    private $insertHyperlinks = false;

    private $deleteColumns = false;

    private $deleteRows = false;

    private $selectLockedCells = false;

    private $sort = false;

    private $autoFilter = false;

    private $pivotTables = false;

    private $selectUnlockedCells = false;

    private $password = '';

    public function __construct()
    {
    }

    public function isProtectionEnabled()
    {
        return $this->sheet ||
            $this->objects ||
            $this->scenarios ||
            $this->formatCells ||
            $this->formatColumns ||
            $this->formatRows ||
            $this->insertColumns ||
            $this->insertRows ||
            $this->insertHyperlinks ||
            $this->deleteColumns ||
            $this->deleteRows ||
            $this->selectLockedCells ||
            $this->sort ||
            $this->autoFilter ||
            $this->pivotTables ||
            $this->selectUnlockedCells;
    }

    public function getSheet()
    {
        return $this->sheet;
    }

    public function setSheet($pValue)
    {
        $this->sheet = $pValue;

        return $this;
    }

    public function getObjects()
    {
        return $this->objects;
    }

    public function setObjects($pValue)
    {
        $this->objects = $pValue;

        return $this;
    }

    public function getScenarios()
    {
        return $this->scenarios;
    }

    public function setScenarios($pValue)
    {
        $this->scenarios = $pValue;

        return $this;
    }

    public function getFormatCells()
    {
        return $this->formatCells;
    }

    public function setFormatCells($pValue)
    {
        $this->formatCells = $pValue;

        return $this;
    }

    public function getFormatColumns()
    {
        return $this->formatColumns;
    }

    public function setFormatColumns($pValue)
    {
        $this->formatColumns = $pValue;

        return $this;
    }

    public function getFormatRows()
    {
        return $this->formatRows;
    }

    public function setFormatRows($pValue)
    {
        $this->formatRows = $pValue;

        return $this;
    }

    public function getInsertColumns()
    {
        return $this->insertColumns;
    }

    public function setInsertColumns($pValue)
    {
        $this->insertColumns = $pValue;

        return $this;
    }

    public function getInsertRows()
    {
        return $this->insertRows;
    }

    public function setInsertRows($pValue)
    {
        $this->insertRows = $pValue;

        return $this;
    }

    public function getInsertHyperlinks()
    {
        return $this->insertHyperlinks;
    }

    public function setInsertHyperlinks($pValue)
    {
        $this->insertHyperlinks = $pValue;

        return $this;
    }

    public function getDeleteColumns()
    {
        return $this->deleteColumns;
    }

    public function setDeleteColumns($pValue)
    {
        $this->deleteColumns = $pValue;

        return $this;
    }

    public function getDeleteRows()
    {
        return $this->deleteRows;
    }

    public function setDeleteRows($pValue)
    {
        $this->deleteRows = $pValue;

        return $this;
    }

    public function getSelectLockedCells()
    {
        return $this->selectLockedCells;
    }

    public function setSelectLockedCells($pValue)
    {
        $this->selectLockedCells = $pValue;

        return $this;
    }

    public function getSort()
    {
        return $this->sort;
    }

    public function setSort($pValue)
    {
        $this->sort = $pValue;

        return $this;
    }

    public function getAutoFilter()
    {
        return $this->autoFilter;
    }

    public function setAutoFilter($pValue)
    {
        $this->autoFilter = $pValue;

        return $this;
    }

    public function getPivotTables()
    {
        return $this->pivotTables;
    }

    public function setPivotTables($pValue)
    {
        $this->pivotTables = $pValue;

        return $this;
    }

    public function getSelectUnlockedCells()
    {
        return $this->selectUnlockedCells;
    }

    public function setSelectUnlockedCells($pValue)
    {
        $this->selectUnlockedCells = $pValue;

        return $this;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function setPassword($pValue, $pAlreadyHashed = false)
    {
        if (!$pAlreadyHashed) {
            $pValue = PasswordHasher::hashPassword($pValue);
        }
        $this->password = $pValue;

        return $this;
    }

    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if (is_object($value)) {
                $this->$key = clone $value;
            } else {
                $this->$key = $value;
            }
        }
    }
}

class Row
{
    private $worksheet;

    private $rowIndex = 0;

    public function __construct(Worksheet $worksheet = null, $rowIndex = 1)
    {
        $this->worksheet = $worksheet;
        $this->rowIndex = $rowIndex;
    }

    public function __destruct()
    {
        unset($this->worksheet);
    }

    public function getRowIndex()
    {
        return $this->rowIndex;
    }

    public function getCellIterator($startColumn = 'A', $endColumn = null)
    {
        return new RowCellIterator($this->worksheet, $this->rowIndex, $startColumn, $endColumn);
    }

    public function getWorksheet()
    {
        return $this->worksheet;
    }
}

class PageSetup
{
    const PAPERSIZE_LETTER = 1;
    const PAPERSIZE_LETTER_SMALL = 2;
    const PAPERSIZE_TABLOID = 3;
    const PAPERSIZE_LEDGER = 4;
    const PAPERSIZE_LEGAL = 5;
    const PAPERSIZE_STATEMENT = 6;
    const PAPERSIZE_EXECUTIVE = 7;
    const PAPERSIZE_A3 = 8;
    const PAPERSIZE_A4 = 9;
    const PAPERSIZE_A4_SMALL = 10;
    const PAPERSIZE_A5 = 11;
    const PAPERSIZE_B4 = 12;
    const PAPERSIZE_B5 = 13;
    const PAPERSIZE_FOLIO = 14;
    const PAPERSIZE_QUARTO = 15;
    const PAPERSIZE_STANDARD_1 = 16;
    const PAPERSIZE_STANDARD_2 = 17;
    const PAPERSIZE_NOTE = 18;
    const PAPERSIZE_NO9_ENVELOPE = 19;
    const PAPERSIZE_NO10_ENVELOPE = 20;
    const PAPERSIZE_NO11_ENVELOPE = 21;
    const PAPERSIZE_NO12_ENVELOPE = 22;
    const PAPERSIZE_NO14_ENVELOPE = 23;
    const PAPERSIZE_C = 24;
    const PAPERSIZE_D = 25;
    const PAPERSIZE_E = 26;
    const PAPERSIZE_DL_ENVELOPE = 27;
    const PAPERSIZE_C5_ENVELOPE = 28;
    const PAPERSIZE_C3_ENVELOPE = 29;
    const PAPERSIZE_C4_ENVELOPE = 30;
    const PAPERSIZE_C6_ENVELOPE = 31;
    const PAPERSIZE_C65_ENVELOPE = 32;
    const PAPERSIZE_B4_ENVELOPE = 33;
    const PAPERSIZE_B5_ENVELOPE = 34;
    const PAPERSIZE_B6_ENVELOPE = 35;
    const PAPERSIZE_ITALY_ENVELOPE = 36;
    const PAPERSIZE_MONARCH_ENVELOPE = 37;
    const PAPERSIZE_6_3_4_ENVELOPE = 38;
    const PAPERSIZE_US_STANDARD_FANFOLD = 39;
    const PAPERSIZE_GERMAN_STANDARD_FANFOLD = 40;
    const PAPERSIZE_GERMAN_LEGAL_FANFOLD = 41;
    const PAPERSIZE_ISO_B4 = 42;
    const PAPERSIZE_JAPANESE_DOUBLE_POSTCARD = 43;
    const PAPERSIZE_STANDARD_PAPER_1 = 44;
    const PAPERSIZE_STANDARD_PAPER_2 = 45;
    const PAPERSIZE_STANDARD_PAPER_3 = 46;
    const PAPERSIZE_INVITE_ENVELOPE = 47;
    const PAPERSIZE_LETTER_EXTRA_PAPER = 48;
    const PAPERSIZE_LEGAL_EXTRA_PAPER = 49;
    const PAPERSIZE_TABLOID_EXTRA_PAPER = 50;
    const PAPERSIZE_A4_EXTRA_PAPER = 51;
    const PAPERSIZE_LETTER_TRANSVERSE_PAPER = 52;
    const PAPERSIZE_A4_TRANSVERSE_PAPER = 53;
    const PAPERSIZE_LETTER_EXTRA_TRANSVERSE_PAPER = 54;
    const PAPERSIZE_SUPERA_SUPERA_A4_PAPER = 55;
    const PAPERSIZE_SUPERB_SUPERB_A3_PAPER = 56;
    const PAPERSIZE_LETTER_PLUS_PAPER = 57;
    const PAPERSIZE_A4_PLUS_PAPER = 58;
    const PAPERSIZE_A5_TRANSVERSE_PAPER = 59;
    const PAPERSIZE_JIS_B5_TRANSVERSE_PAPER = 60;
    const PAPERSIZE_A3_EXTRA_PAPER = 61;
    const PAPERSIZE_A5_EXTRA_PAPER = 62;
    const PAPERSIZE_ISO_B5_EXTRA_PAPER = 63;
    const PAPERSIZE_A2_PAPER = 64;
    const PAPERSIZE_A3_TRANSVERSE_PAPER = 65;
    const PAPERSIZE_A3_EXTRA_TRANSVERSE_PAPER = 66;

    const ORIENTATION_DEFAULT = 'default';
    const ORIENTATION_LANDSCAPE = 'landscape';
    const ORIENTATION_PORTRAIT = 'portrait';

    const SETPRINTRANGE_OVERWRITE = 'O';
    const SETPRINTRANGE_INSERT = 'I';

    private $paperSize = self::PAPERSIZE_LETTER;

    private $orientation = self::ORIENTATION_DEFAULT;

    private $scale = 100;

    private $fitToPage = false;

    private $fitToHeight = 1;

    private $fitToWidth = 1;

    private $columnsToRepeatAtLeft = ['', ''];

    private $rowsToRepeatAtTop = [0, 0];

    private $horizontalCentered = false;

    private $verticalCentered = false;

    private $printArea;

    private $firstPageNumber;

    public function __construct()
    {
    }

    public function getPaperSize()
    {
        return $this->paperSize;
    }

    public function setPaperSize($pValue)
    {
        $this->paperSize = $pValue;

        return $this;
    }

    public function getOrientation()
    {
        return $this->orientation;
    }

    public function setOrientation($pValue)
    {
        $this->orientation = $pValue;

        return $this;
    }

    public function getScale()
    {
        return $this->scale;
    }

    public function setScale($pValue, $pUpdate = true)
    {
        if (($pValue >= 0) || $pValue === null) {
            $this->scale = $pValue;
            if ($pUpdate) {
                $this->fitToPage = false;
            }
        } else {
            throw new GyhReadsheetException('Scale must not be negative');
        }

        return $this;
    }

    public function getFitToPage()
    {
        return $this->fitToPage;
    }

    public function setFitToPage($pValue)
    {
        $this->fitToPage = $pValue;

        return $this;
    }

    public function getFitToHeight()
    {
        return $this->fitToHeight;
    }

    public function setFitToHeight($pValue, $pUpdate = true)
    {
        $this->fitToHeight = $pValue;
        if ($pUpdate) {
            $this->fitToPage = true;
        }

        return $this;
    }

    public function getFitToWidth()
    {
        return $this->fitToWidth;
    }

    public function setFitToWidth($pValue, $pUpdate = true)
    {
        $this->fitToWidth = $pValue;
        if ($pUpdate) {
            $this->fitToPage = true;
        }

        return $this;
    }

    public function isColumnsToRepeatAtLeftSet()
    {
        if (is_array($this->columnsToRepeatAtLeft)) {
            if ($this->columnsToRepeatAtLeft[0] != '' && $this->columnsToRepeatAtLeft[1] != '') {
                return true;
            }
        }

        return false;
    }

    public function getColumnsToRepeatAtLeft()
    {
        return $this->columnsToRepeatAtLeft;
    }

    public function setColumnsToRepeatAtLeft(array $pValue)
    {
        $this->columnsToRepeatAtLeft = $pValue;

        return $this;
    }

    public function setColumnsToRepeatAtLeftByStartAndEnd($pStart, $pEnd)
    {
        $this->columnsToRepeatAtLeft = [$pStart, $pEnd];

        return $this;
    }

    public function isRowsToRepeatAtTopSet()
    {
        if (is_array($this->rowsToRepeatAtTop)) {
            if ($this->rowsToRepeatAtTop[0] != 0 && $this->rowsToRepeatAtTop[1] != 0) {
                return true;
            }
        }

        return false;
    }

    public function getRowsToRepeatAtTop()
    {
        return $this->rowsToRepeatAtTop;
    }

    public function setRowsToRepeatAtTop(array $pValue)
    {
        $this->rowsToRepeatAtTop = $pValue;

        return $this;
    }

    public function setRowsToRepeatAtTopByStartAndEnd($pStart, $pEnd)
    {
        $this->rowsToRepeatAtTop = [$pStart, $pEnd];

        return $this;
    }

    public function getHorizontalCentered()
    {
        return $this->horizontalCentered;
    }

    public function setHorizontalCentered($value)
    {
        $this->horizontalCentered = $value;

        return $this;
    }

    public function getVerticalCentered()
    {
        return $this->verticalCentered;
    }

    public function setVerticalCentered($value)
    {
        $this->verticalCentered = $value;

        return $this;
    }

    public function getPrintArea($index = 0)
    {
        if ($index == 0) {
            return $this->printArea;
        }
        $printAreas = explode(',', $this->printArea);
        if (isset($printAreas[$index - 1])) {
            return $printAreas[$index - 1];
        }

        throw new GyhReadsheetException('Requested Print Area does not exist');
    }

    public function isPrintAreaSet($index = 0)
    {
        if ($index == 0) {
            return $this->printArea !== null;
        }
        $printAreas = explode(',', $this->printArea);

        return isset($printAreas[$index - 1]);
    }

    public function clearPrintArea($index = 0)
    {
        if ($index == 0) {
            $this->printArea = null;
        } else {
            $printAreas = explode(',', $this->printArea);
            if (isset($printAreas[$index - 1])) {
                unset($printAreas[$index - 1]);
                $this->printArea = implode(',', $printAreas);
            }
        }

        return $this;
    }

    public function setPrintArea($value, $index = 0, $method = self::SETPRINTRANGE_OVERWRITE)
    {
        if (strpos($value, '!') !== false) {
            throw new GyhReadsheetException('Cell coordinate must not specify a worksheet.');
        } elseif (strpos($value, ':') === false) {
            throw new GyhReadsheetException('Cell coordinate must be a range of cells.');
        } elseif (strpos($value, '$') !== false) {
            throw new GyhReadsheetException('Cell coordinate must not be absolute.');
        }
        $value = strtoupper($value);

        if ($method == self::SETPRINTRANGE_OVERWRITE) {
            if ($index == 0) {
                $this->printArea = $value;
            } else {
                $printAreas = explode(',', $this->printArea);
                if ($index < 0) {
                    $index = count($printAreas) - abs($index) + 1;
                }
                if (($index <= 0) || ($index > count($printAreas))) {
                    throw new GyhReadsheetException('Invalid index for setting print range.');
                }
                $printAreas[$index - 1] = $value;
                $this->printArea = implode(',', $printAreas);
            }
        } elseif ($method == self::SETPRINTRANGE_INSERT) {
            if ($index == 0) {
                $this->printArea .= ($this->printArea == '') ? $value : ',' . $value;
            } else {
                $printAreas = explode(',', $this->printArea);
                if ($index < 0) {
                    $index = abs($index) - 1;
                }
                if ($index > count($printAreas)) {
                    throw new GyhReadsheetException('Invalid index for setting print range.');
                }
                $printAreas = array_merge(array_slice($printAreas, 0, $index), [$value], array_slice($printAreas, $index));
                $this->printArea = implode(',', $printAreas);
            }
        } else {
            throw new GyhReadsheetException('Invalid method for setting print range.');
        }

        return $this;
    }

    public function addPrintArea($value, $index = -1)
    {
        return $this->setPrintArea($value, $index, self::SETPRINTRANGE_INSERT);
    }

    public function setPrintAreaByColumnAndRow($column1, $row1, $column2, $row2, $index = 0, $method = self::SETPRINTRANGE_OVERWRITE)
    {
        return $this->setPrintArea(
            Coordinate::stringFromColumnIndex($column1) . $row1 . ':' . Coordinate::stringFromColumnIndex($column2) . $row2,
            $index,
            $method
        );
    }

    public function addPrintAreaByColumnAndRow($column1, $row1, $column2, $row2, $index = -1)
    {
        return $this->setPrintArea(
            Coordinate::stringFromColumnIndex($column1) . $row1 . ':' . Coordinate::stringFromColumnIndex($column2) . $row2,
            $index,
            self::SETPRINTRANGE_INSERT
        );
    }

    public function getFirstPageNumber()
    {
        return $this->firstPageNumber;
    }

    public function setFirstPageNumber($value)
    {
        $this->firstPageNumber = $value;

        return $this;
    }

    public function resetFirstPageNumber()
    {
        return $this->setFirstPageNumber(null);
    }

    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if (is_object($value)) {
                $this->$key = clone $value;
            } else {
                $this->$key = $value;
            }
        }
    }
}


class PageMargins
{
    private $left = 0.7;

    private $right = 0.7;

    private $top = 0.75;

    private $bottom = 0.75;

    private $header = 0.3;

    private $footer = 0.3;

    public function __construct()
    {
    }

    public function getLeft()
    {
        return $this->left;
    }

    public function setLeft($pValue)
    {
        $this->left = $pValue;

        return $this;
    }

    public function getRight()
    {
        return $this->right;
    }

    public function setRight($pValue)
    {
        $this->right = $pValue;

        return $this;
    }

    public function getTop()
    {
        return $this->top;
    }

    public function setTop($pValue)
    {
        $this->top = $pValue;

        return $this;
    }

    public function getBottom()
    {
        return $this->bottom;
    }

    public function setBottom($pValue)
    {
        $this->bottom = $pValue;

        return $this;
    }

    public function getHeader()
    {
        return $this->header;
    }

    public function setHeader($pValue)
    {
        $this->header = $pValue;

        return $this;
    }

    public function getFooter()
    {
        return $this->footer;
    }

    public function setFooter($pValue)
    {
        $this->footer = $pValue;

        return $this;
    }

    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if (is_object($value)) {
                $this->$key = clone $value;
            } else {
                $this->$key = $value;
            }
        }
    }
}

class HeaderFooter
{
    const IMAGE_HEADER_LEFT = 'LH';
    const IMAGE_HEADER_CENTER = 'CH';
    const IMAGE_HEADER_RIGHT = 'RH';
    const IMAGE_FOOTER_LEFT = 'LF';
    const IMAGE_FOOTER_CENTER = 'CF';
    const IMAGE_FOOTER_RIGHT = 'RF';

    private $oddHeader = '';

    private $oddFooter = '';

    private $evenHeader = '';

    private $evenFooter = '';

    private $firstHeader = '';

    private $firstFooter = '';

    private $differentOddEven = false;

    private $differentFirst = false;

    private $scaleWithDocument = true;

    private $alignWithMargins = true;

    private $headerFooterImages = [];

    public function __construct()
    {
    }

    public function getOddHeader()
    {
        return $this->oddHeader;
    }

    public function setOddHeader($pValue)
    {
        $this->oddHeader = $pValue;

        return $this;
    }

    public function getOddFooter()
    {
        return $this->oddFooter;
    }

    public function setOddFooter($pValue)
    {
        $this->oddFooter = $pValue;

        return $this;
    }

    public function getEvenHeader()
    {
        return $this->evenHeader;
    }

    public function setEvenHeader($pValue)
    {
        $this->evenHeader = $pValue;

        return $this;
    }

    public function getEvenFooter()
    {
        return $this->evenFooter;
    }

    public function setEvenFooter($pValue)
    {
        $this->evenFooter = $pValue;

        return $this;
    }

    public function getFirstHeader()
    {
        return $this->firstHeader;
    }

    public function setFirstHeader($pValue)
    {
        $this->firstHeader = $pValue;

        return $this;
    }

    public function getFirstFooter()
    {
        return $this->firstFooter;
    }

    public function setFirstFooter($pValue)
    {
        $this->firstFooter = $pValue;

        return $this;
    }

    public function getDifferentOddEven()
    {
        return $this->differentOddEven;
    }

    public function setDifferentOddEven($pValue)
    {
        $this->differentOddEven = $pValue;

        return $this;
    }

    public function getDifferentFirst()
    {
        return $this->differentFirst;
    }

    public function setDifferentFirst($pValue)
    {
        $this->differentFirst = $pValue;

        return $this;
    }

    public function getScaleWithDocument()
    {
        return $this->scaleWithDocument;
    }

    public function setScaleWithDocument($pValue)
    {
        $this->scaleWithDocument = $pValue;

        return $this;
    }

    public function getAlignWithMargins()
    {
        return $this->alignWithMargins;
    }

    public function setAlignWithMargins($pValue)
    {
        $this->alignWithMargins = $pValue;

        return $this;
    }

    public function addImage(HeaderFooterDrawing $image, $location = self::IMAGE_HEADER_LEFT)
    {
        $this->headerFooterImages[$location] = $image;

        return $this;
    }

    public function removeImage($location = self::IMAGE_HEADER_LEFT)
    {
        if (isset($this->headerFooterImages[$location])) {
            unset($this->headerFooterImages[$location]);
        }

        return $this;
    }

    public function setImages(array $images)
    {
        $this->headerFooterImages = $images;

        return $this;
    }

    public function getImages()
    {
        $images = [];
        if (isset($this->headerFooterImages[self::IMAGE_HEADER_LEFT])) {
            $images[self::IMAGE_HEADER_LEFT] = $this->headerFooterImages[self::IMAGE_HEADER_LEFT];
        }
        if (isset($this->headerFooterImages[self::IMAGE_HEADER_CENTER])) {
            $images[self::IMAGE_HEADER_CENTER] = $this->headerFooterImages[self::IMAGE_HEADER_CENTER];
        }
        if (isset($this->headerFooterImages[self::IMAGE_HEADER_RIGHT])) {
            $images[self::IMAGE_HEADER_RIGHT] = $this->headerFooterImages[self::IMAGE_HEADER_RIGHT];
        }
        if (isset($this->headerFooterImages[self::IMAGE_FOOTER_LEFT])) {
            $images[self::IMAGE_FOOTER_LEFT] = $this->headerFooterImages[self::IMAGE_FOOTER_LEFT];
        }
        if (isset($this->headerFooterImages[self::IMAGE_FOOTER_CENTER])) {
            $images[self::IMAGE_FOOTER_CENTER] = $this->headerFooterImages[self::IMAGE_FOOTER_CENTER];
        }
        if (isset($this->headerFooterImages[self::IMAGE_FOOTER_RIGHT])) {
            $images[self::IMAGE_FOOTER_RIGHT] = $this->headerFooterImages[self::IMAGE_FOOTER_RIGHT];
        }
        $this->headerFooterImages = $images;

        return $this->headerFooterImages;
    }

    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if (is_object($value)) {
                $this->$key = clone $value;
            } else {
                $this->$key = $value;
            }
        }
    }
}

abstract class Dimension
{
    private $visible = true;

    private $outlineLevel = 0;

    private $collapsed = false;

    private $xfIndex;

    public function __construct($initialValue = null)
    {
        $this->xfIndex = $initialValue;
    }

    public function getVisible()
    {
        return $this->visible;
    }

    public function setVisible($pValue)
    {
        $this->visible = $pValue;

        return $this;
    }

    public function getOutlineLevel()
    {
        return $this->outlineLevel;
    }

    public function setOutlineLevel($pValue)
    {
        if ($pValue < 0 || $pValue > 7) {
            throw new GyhReadsheetException('Outline level must range between 0 and 7.');
        }

        $this->outlineLevel = $pValue;

        return $this;
    }

    public function getCollapsed()
    {
        return $this->collapsed;
    }

    public function setCollapsed($pValue)
    {
        $this->collapsed = $pValue;

        return $this;
    }

    public function getXfIndex()
    {
        return $this->xfIndex;
    }

    public function setXfIndex($pValue)
    {
        $this->xfIndex = $pValue;

        return $this;
    }

    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if (is_object($value)) {
                $this->$key = clone $value;
            } else {
                $this->$key = $value;
            }
        }
    }
}


class AutoFilter
{
    private $workSheet;

    private $range = '';

    private $columns = [];

    public function __construct($pRange = '', Worksheet $pSheet = null)
    {
        $this->range = $pRange;
        $this->workSheet = $pSheet;
    }

    public function getParent()
    {
        return $this->workSheet;
    }

    public function setParent(Worksheet $pSheet = null)
    {
        $this->workSheet = $pSheet;

        return $this;
    }

    public function getRange()
    {
        return $this->range;
    }

    public function setRange($pRange)
    {
        list($worksheet, $pRange) = Worksheet::extractSheetTitle($pRange, true);

        if (strpos($pRange, ':') !== false) {
            $this->range = $pRange;
        } elseif (empty($pRange)) {
            $this->range = '';
        } else {
            throw new GyhReadsheetException('Autofilter must be set on a range of cells.');
        }

        if (empty($pRange)) {
            $this->columns = [];
        } else {
            list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range);
            foreach ($this->columns as $key => $value) {
                $colIndex = Coordinate::columnIndexFromString($key);
                if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
                    unset($this->columns[$key]);
                }
            }
        }

        return $this;
    }

    public function getColumns()
    {
        return $this->columns;
    }

    public function testColumnInRange($column)
    {
        if (empty($this->range)) {
            throw new GyhReadsheetException('No autofilter range is defined.');
        }

        $columnIndex = Coordinate::columnIndexFromString($column);
        list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range);
        if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
            throw new GyhReadsheetException('Column is outside of current autofilter range.');
        }

        return $columnIndex - $rangeStart[0];
    }

    public function getColumnOffset($pColumn)
    {
        return $this->testColumnInRange($pColumn);
    }

    public function getColumn($pColumn)
    {
        $this->testColumnInRange($pColumn);

        if (!isset($this->columns[$pColumn])) {
            $this->columns[$pColumn] = new AutoFilter\Column($pColumn, $this);
        }

        return $this->columns[$pColumn];
    }

    public function getColumnByOffset($pColumnOffset)
    {
        list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range);
        $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $pColumnOffset);

        return $this->getColumn($pColumn);
    }

    public function setColumn($pColumn)
    {
        if ((is_string($pColumn)) && (!empty($pColumn))) {
            $column = $pColumn;
        } elseif (is_object($pColumn) && ($pColumn instanceof AutoFilter\Column)) {
            $column = $pColumn->getColumnIndex();
        } else {
            throw new GyhReadsheetException('Column is not within the autofilter range.');
        }
        $this->testColumnInRange($column);

        if (is_string($pColumn)) {
            $this->columns[$pColumn] = new AutoFilter\Column($pColumn, $this);
        } elseif (is_object($pColumn) && ($pColumn instanceof AutoFilter\Column)) {
            $pColumn->setParent($this);
            $this->columns[$column] = $pColumn;
        }
        ksort($this->columns);

        return $this;
    }

    public function clearColumn($pColumn)
    {
        $this->testColumnInRange($pColumn);

        if (isset($this->columns[$pColumn])) {
            unset($this->columns[$pColumn]);
        }

        return $this;
    }

    public function shiftColumn($fromColumn, $toColumn)
    {
        $fromColumn = strtoupper($fromColumn);
        $toColumn = strtoupper($toColumn);

        if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) {
            $this->columns[$fromColumn]->setParent();
            $this->columns[$fromColumn]->setColumnIndex($toColumn);
            $this->columns[$toColumn] = $this->columns[$fromColumn];
            $this->columns[$toColumn]->setParent($this);
            unset($this->columns[$fromColumn]);

            ksort($this->columns);
        }

        return $this;
    }

    private static function filterTestInSimpleDataSet($cellValue, $dataSet)
    {
        $dataSetValues = $dataSet['filterValues'];
        $blanks = $dataSet['blanks'];
        if (($cellValue == '') || ($cellValue === null)) {
            return $blanks;
        }

        return in_array($cellValue, $dataSetValues);
    }

    private static function filterTestInDateGroupSet($cellValue, $dataSet)
    {
        $dateSet = $dataSet['filterValues'];
        $blanks = $dataSet['blanks'];
        if (($cellValue == '') || ($cellValue === null)) {
            return $blanks;
        }

        if (is_numeric($cellValue)) {
            $dateValue = Date::excelToTimestamp($cellValue);
            if ($cellValue < 1) {
                $dtVal = date('His', $dateValue);
                $dateSet = $dateSet['time'];
            } elseif ($cellValue == floor($cellValue)) {
                $dtVal = date('Ymd', $dateValue);
                $dateSet = $dateSet['date'];
            } else {
                $dtVal = date('YmdHis', $dateValue);
                $dateSet = $dateSet['dateTime'];
            }
            foreach ($dateSet as $dateValue) {
                if (substr($dtVal, 0, strlen($dateValue)) == $dateValue) {
                    return true;
                }
            }
        }

        return false;
    }

    private static function filterTestInCustomDataSet($cellValue, $ruleSet)
    {
        $dataSet = $ruleSet['filterRules'];
        $join = $ruleSet['join'];
        $customRuleForBlanks = isset($ruleSet['customRuleForBlanks']) ? $ruleSet['customRuleForBlanks'] : false;

        if (!$customRuleForBlanks) {
            if (($cellValue == '') || ($cellValue === null)) {
                return false;
            }
        }
        $returnVal = ($join == AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND);
        foreach ($dataSet as $rule) {
            $retVal = false;

            if (is_numeric($rule['value'])) {
                switch ($rule['operator']) {
                    case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
                        $retVal = ($cellValue == $rule['value']);

                        break;
                    case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
                        $retVal = ($cellValue != $rule['value']);

                        break;
                    case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
                        $retVal = ($cellValue > $rule['value']);

                        break;
                    case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
                        $retVal = ($cellValue >= $rule['value']);

                        break;
                    case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
                        $retVal = ($cellValue < $rule['value']);

                        break;
                    case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
                        $retVal = ($cellValue <= $rule['value']);

                        break;
                }
            } elseif ($rule['value'] == '') {
                switch ($rule['operator']) {
                    case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
                        $retVal = (($cellValue == '') || ($cellValue === null));

                        break;
                    case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
                        $retVal = (($cellValue != '') && ($cellValue !== null));

                        break;
                    default:
                        $retVal = true;

                        break;
                }
            } else {
                $retVal = preg_match('/^' . $rule['value'] . '$/i', $cellValue);
            }
            switch ($join) {
                case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR:
                    $returnVal = $returnVal || $retVal;
                    if ($returnVal) {
                        return $returnVal;
                    }

                    break;
                case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND:
                    $returnVal = $returnVal && $retVal;

                    break;
            }
        }

        return $returnVal;
    }

    private static function filterTestInPeriodDateSet($cellValue, $monthSet)
    {
        if (($cellValue == '') || ($cellValue === null)) {
            return false;
        }

        if (is_numeric($cellValue)) {
            $dateValue = date('m', Date::excelToTimestamp($cellValue));
            if (in_array($dateValue, $monthSet)) {
                return true;
            }
        }

        return false;
    }

    private static $fromReplace = ['\*', '\?', '~~', '~.*', '~.?'];

    private static $toReplace = ['.*', '.', '~', '\*', '\?'];

    private function dynamicFilterDateRange($dynamicRuleType, &$filterColumn)
    {
        $rDateType = Functions::getReturnDateType();
        Functions::setReturnDateType(Functions::RETURNDATE_PHP_NUMERIC);
        $val = $maxVal = null;

        $ruleValues = [];
        $baseDate = DateTime::DATENOW();
        switch ($dynamicRuleType) {
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK:
                $baseDate = strtotime('-7 days', $baseDate);

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK:
                $baseDate = strtotime('-7 days', $baseDate);

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH:
                $baseDate = strtotime('-1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH:
                $baseDate = strtotime('+1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER:
                $baseDate = strtotime('-3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER:
                $baseDate = strtotime('+3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR:
                $baseDate = strtotime('-1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR:
                $baseDate = strtotime('+1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));

                break;
        }

        switch ($dynamicRuleType) {
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW:
                $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate));
                $val = (int) Date::PHPToExcel($baseDate);

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE:
                $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate));
                $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate)));

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR:
                $maxVal = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 31, 12, date('Y', $baseDate)));
                ++$maxVal;
                $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate)));

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER:
                $thisMonth = date('m', $baseDate);
                $thisQuarter = floor(--$thisMonth / 3);
                $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), (1 + $thisQuarter) * 3, date('Y', $baseDate)));
                ++$maxVal;
                $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1 + $thisQuarter * 3, date('Y', $baseDate)));

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH:
                $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), date('m', $baseDate), date('Y', $baseDate)));
                ++$maxVal;
                $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK:
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK:
                $dayOfWeek = date('w', $baseDate);
                $val = (int) Date::PHPToExcel($baseDate) - $dayOfWeek;
                $maxVal = $val + 7;

                break;
        }

        switch ($dynamicRuleType) {
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY:
                --$maxVal;
                --$val;

                break;
            case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW:
                ++$maxVal;
                ++$val;

                break;
        }

        $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxVal]);

        $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val];
        $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxVal];
        Functions::setReturnDateType($rDateType);

        return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]];
    }

    private function calculateTopTenValue($columnID, $startRow, $endRow, $ruleType, $ruleValue)
    {
        $range = $columnID . $startRow . ':' . $columnID . $endRow;
        $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false));

        $dataValues = array_filter($dataValues);
        if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) {
            rsort($dataValues);
        } else {
            sort($dataValues);
        }

        return array_pop(array_slice($dataValues, 0, $ruleValue));
    }

    public function showHideRows()
    {
        list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range);

        $this->workSheet->getRowDimension($rangeStart[1])->setVisible(true);

        $columnFilterTests = [];
        foreach ($this->columns as $columnID => $filterColumn) {
            $rules = $filterColumn->getRules();
            switch ($filterColumn->getFilterType()) {
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER:
                    $ruleType = null;
                    $ruleValues = [];
                    foreach ($rules as $rule) {
                        $ruleType = $rule->getRuleType();
                        $ruleValues[] = $rule->getValue();
                    }
                    $blanks = false;
                    $ruleDataSet = array_filter($ruleValues);
                    if (count($ruleValues) != count($ruleDataSet)) {
                        $blanks = true;
                    }
                    if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_FILTER) {
                        $columnFilterTests[$columnID] = [
                            'method' => 'filterTestInSimpleDataSet',
                            'arguments' => ['filterValues' => $ruleDataSet, 'blanks' => $blanks],
                        ];
                    } else {
                        $arguments = [
                            'date' => [],
                            'time' => [],
                            'dateTime' => [],
                        ];
                        foreach ($ruleDataSet as $ruleValue) {
                            $date = $time = '';
                            if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) &&
                                ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '')) {
                                $date .= sprintf('%04d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]);
                            }
                            if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) &&
                                ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '')) {
                                $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]);
                            }
                            if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) &&
                                ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '')) {
                                $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]);
                            }
                            if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) &&
                                ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '')) {
                                $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]);
                            }
                            if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) &&
                                ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '')) {
                                $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]);
                            }
                            if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) &&
                                ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '')) {
                                $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]);
                            }
                            $dateTime = $date . $time;
                            $arguments['date'][] = $date;
                            $arguments['time'][] = $time;
                            $arguments['dateTime'][] = $dateTime;
                        }
                        $arguments['date'] = array_filter($arguments['date']);
                        $arguments['time'] = array_filter($arguments['time']);
                        $arguments['dateTime'] = array_filter($arguments['dateTime']);
                        $columnFilterTests[$columnID] = [
                            'method' => 'filterTestInDateGroupSet',
                            'arguments' => ['filterValues' => $arguments, 'blanks' => $blanks],
                        ];
                    }

                    break;
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER:
                    $customRuleForBlanks = false;
                    $ruleValues = [];
                    foreach ($rules as $rule) {
                        $ruleValue = $rule->getValue();
                        if (!is_numeric($ruleValue)) {
                            $ruleValue = preg_quote($ruleValue);
                            $ruleValue = str_replace(self::$fromReplace, self::$toReplace, $ruleValue);
                            if (trim($ruleValue) == '') {
                                $customRuleForBlanks = true;
                                $ruleValue = trim($ruleValue);
                            }
                        }
                        $ruleValues[] = ['operator' => $rule->getOperator(), 'value' => $ruleValue];
                    }
                    $join = $filterColumn->getJoin();
                    $columnFilterTests[$columnID] = [
                        'method' => 'filterTestInCustomDataSet',
                        'arguments' => ['filterRules' => $ruleValues, 'join' => $join, 'customRuleForBlanks' => $customRuleForBlanks],
                    ];

                    break;
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER:
                    $ruleValues = [];
                    foreach ($rules as $rule) {
                        $dynamicRuleType = $rule->getGrouping();
                        if (($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) ||
                            ($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE)) {
                            $averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')';
                            $average = Calculation::getInstance()->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1'));
                            $operator = ($dynamicRuleType === AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)
                                ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN
                                : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN;
                            $ruleValues[] = [
                                'operator' => $operator,
                                'value' => $average,
                            ];
                            $columnFilterTests[$columnID] = [
                                'method' => 'filterTestInCustomDataSet',
                                'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
                            ];
                        } else {
                            if ($dynamicRuleType[0] == 'M' || $dynamicRuleType[0] == 'Q') {
                                $periodType = '';
                                $period = 0;
                                sscanf($dynamicRuleType, '%[A-Z]%d', $periodType, $period);
                                if ($periodType == 'M') {
                                    $ruleValues = [$period];
                                } else {
                                    --$period;
                                    $periodEnd = (1 + $period) * 3;
                                    $periodStart = 1 + $period * 3;
                                    $ruleValues = range($periodStart, $periodEnd);
                                }
                                $columnFilterTests[$columnID] = [
                                    'method' => 'filterTestInPeriodDateSet',
                                    'arguments' => $ruleValues,
                                ];
                                $filterColumn->setAttributes([]);
                            } else {
                                $columnFilterTests[$columnID] = $this->dynamicFilterDateRange($dynamicRuleType, $filterColumn);

                                break;
                            }
                        }
                    }

                    break;
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER:
                    $ruleValues = [];
                    $dataRowCount = $rangeEnd[1] - $rangeStart[1];
                    foreach ($rules as $rule) {
                        $toptenRuleType = $rule->getGrouping();
                        $ruleValue = $rule->getValue();
                        $ruleOperator = $rule->getOperator();
                    }
                    if ($ruleOperator === AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) {
                        $ruleValue = floor($ruleValue * ($dataRowCount / 100));
                    }
                    if ($ruleValue < 1) {
                        $ruleValue = 1;
                    }
                    if ($ruleValue > 500) {
                        $ruleValue = 500;
                    }

                    $maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, $rangeEnd[1], $toptenRuleType, $ruleValue);

                    $operator = ($toptenRuleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP)
                        ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL
                        : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL;
                    $ruleValues[] = ['operator' => $operator, 'value' => $maxVal];
                    $columnFilterTests[$columnID] = [
                        'method' => 'filterTestInCustomDataSet',
                        'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
                    ];
                    $filterColumn->setAttributes(['maxVal' => $maxVal]);

                    break;
            }
        }

        for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) {
            $result = true;
            foreach ($columnFilterTests as $columnID => $columnFilterTest) {
                $cellValue = $this->workSheet->getCell($columnID . $row)->getCalculatedValue();
                $result = $result &&
                    call_user_func_array(
                        [self::class, $columnFilterTest['method']],
                        [$cellValue, $columnFilterTest['arguments']]
                    );
                if (!$result) {
                    break;
                }
            }
            $this->workSheet->getRowDimension($row)->setVisible($result);
        }

        return $this;
    }

    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if (is_object($value)) {
                if ($key === 'workSheet') {
                    $this->{$key} = null;
                } else {
                    $this->{$key} = clone $value;
                }
            } elseif ((is_array($value)) && ($key == 'columns')) {
                $this->{$key} = [];
                foreach ($value as $k => $v) {
                    $this->{$key}[$k] = clone $v;
                    $this->{$key}[$k]->setParent($this);
                }
            } else {
                $this->{$key} = $value;
            }
        }
    }

    public function __toString()
    {
        return (string) $this->range;
    }
}


class ColumnDimension extends Dimension
{
    private $columnIndex;

    private $width = -1;

    private $autoSize = false;

    public function __construct($pIndex = 'A')
    {
        $this->columnIndex = $pIndex;

        parent::__construct(0);
    }

    public function getColumnIndex()
    {
        return $this->columnIndex;
    }

    public function setColumnIndex($pValue)
    {
        $this->columnIndex = $pValue;

        return $this;
    }

    public function getWidth()
    {
        return $this->width;
    }

    public function setWidth($pValue)
    {
        $this->width = $pValue;

        return $this;
    }

    public function getAutoSize()
    {
        return $this->autoSize;
    }

    public function setAutoSize($pValue)
    {
        $this->autoSize = $pValue;

        return $this;
    }
}


class Worksheet implements IComparable
{
    const BREAK_NONE = 0;
    const BREAK_ROW = 1;
    const BREAK_COLUMN = 2;

    const SHEETSTATE_VISIBLE = 'visible';
    const SHEETSTATE_HIDDEN = 'hidden';
    const SHEETSTATE_VERYHIDDEN = 'veryHidden';

    const SHEET_TITLE_MAXIMUM_LENGTH = 31;

    private static $invalidCharacters = ['*', ':', '/', '\\', '?', '[', ']'];

    private $parent;

    private $cellCollection;

    private $rowDimensions = [];

    private $defaultRowDimension;

    private $columnDimensions = [];

    private $defaultColumnDimension;

    private $drawingCollection;

    private $chartCollection = [];

    private $title;

    private $sheetState;

    private $pageSetup;

    private $pageMargins;

    private $headerFooter;

    private $sheetView;

    private $protection;

    private $styles = [];

    private $conditionalStylesCollection = [];

    private $cellCollectionIsSorted = false;

    private $breaks = [];

    private $mergeCells = [];

    private $protectedCells = [];

    private $autoFilter;

    private $freezePane;

    private $topLeftCell;

    private $showGridlines = true;

    private $printGridlines = false;

    private $showRowColHeaders = true;

    private $showSummaryBelow = true;

    private $showSummaryRight = true;

    private $comments = [];

    private $activeCell = 'A1';

    private $selectedCells = 'A1';

    private $cachedHighestColumn = 'A';

    private $cachedHighestRow = 1;

    private $rightToLeft = false;

    private $hyperlinkCollection = [];

    private $dataValidationCollection = [];

    private $tabColor;

    private $dirty = true;

    private $hash;

    private $codeName;

    public function __construct(Spreadsheet $parent = null, $pTitle = 'Worksheet')
    {
        $this->parent = $parent;
        $this->setTitle($pTitle, false);
        $this->setCodeName($this->getTitle());
        $this->setSheetState(self::SHEETSTATE_VISIBLE);

        $this->cellCollection = CellsFactory::getInstance($this);
        $this->pageSetup = new PageSetup();
        $this->pageMargins = new PageMargins();
        $this->headerFooter = new HeaderFooter();
        $this->sheetView = new SheetView();
        $this->drawingCollection = new \ArrayObject();
        $this->chartCollection = new \ArrayObject();
        $this->protection = new Protection();
        $this->defaultRowDimension = new RowDimension(null);
        $this->defaultColumnDimension = new ColumnDimension(null);
        $this->autoFilter = new AutoFilter(null, $this);
    }

    public function disconnectCells()
    {
        if ($this->cellCollection !== null) {
            $this->cellCollection->unsetWorksheetCells();
            $this->cellCollection = null;
        }
        $this->parent = null;
    }

    public function __destruct()
    {
        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);

        $this->disconnectCells();
    }

    public function getCellCollection()
    {
        return $this->cellCollection;
    }

    public static function getInvalidCharacters()
    {
        return self::$invalidCharacters;
    }

    private static function checkSheetCodeName($pValue)
    {
        $CharCount = Shared\StringHelper::countCharacters($pValue);
        if ($CharCount == 0) {
            throw new Exception('Sheet code name cannot be empty.');
        }
        if ((str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) ||
            (Shared\StringHelper::substring($pValue, -1, 1) == '\'') ||
            (Shared\StringHelper::substring($pValue, 0, 1) == '\'')) {
            throw new Exception('Invalid character found in sheet code name');
        }

        if ($CharCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
        }

        return $pValue;
    }

    private static function checkSheetTitle($pValue)
    {
        if (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) {
            throw new Exception('Invalid character found in sheet title');
        }

        if (Shared\StringHelper::countCharacters($pValue) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
        }

        return $pValue;
    }

    public function getCoordinates($sorted = true)
    {
        if ($this->cellCollection == null) {
            return [];
        }

        if ($sorted) {
            return $this->cellCollection->getSortedCoordinates();
        }

        return $this->cellCollection->getCoordinates();
    }

    public function getRowDimensions()
    {
        return $this->rowDimensions;
    }

    public function getDefaultRowDimension()
    {
        return $this->defaultRowDimension;
    }

    public function getColumnDimensions()
    {
        return $this->columnDimensions;
    }

    public function getDefaultColumnDimension()
    {
        return $this->defaultColumnDimension;
    }

    public function getDrawingCollection()
    {
        return $this->drawingCollection;
    }

    public function getChartCollection()
    {
        return $this->chartCollection;
    }

    public function addChart(Chart $pChart, $iChartIndex = null)
    {
        $pChart->setWorksheet($this);
        if ($iChartIndex === null) {
            $this->chartCollection[] = $pChart;
        } else {
            array_splice($this->chartCollection, $iChartIndex, 0, [$pChart]);
        }

        return $pChart;
    }

    public function getChartCount()
    {
        return count($this->chartCollection);
    }

    public function getChartByIndex($index)
    {
        $chartCount = count($this->chartCollection);
        if ($chartCount == 0) {
            return false;
        }
        if ($index === null) {
            $index = --$chartCount;
        }
        if (!isset($this->chartCollection[$index])) {
            return false;
        }

        return $this->chartCollection[$index];
    }

    public function getChartNames()
    {
        $chartNames = [];
        foreach ($this->chartCollection as $chart) {
            $chartNames[] = $chart->getName();
        }

        return $chartNames;
    }

    public function getChartByName($chartName)
    {
        $chartCount = count($this->chartCollection);
        if ($chartCount == 0) {
            return false;
        }
        foreach ($this->chartCollection as $index => $chart) {
            if ($chart->getName() == $chartName) {
                return $this->chartCollection[$index];
            }
        }

        return false;
    }

    public function refreshColumnDimensions()
    {
        $currentColumnDimensions = $this->getColumnDimensions();
        $newColumnDimensions = [];

        foreach ($currentColumnDimensions as $objColumnDimension) {
            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
        }

        $this->columnDimensions = $newColumnDimensions;

        return $this;
    }

    public function refreshRowDimensions()
    {
        $currentRowDimensions = $this->getRowDimensions();
        $newRowDimensions = [];

        foreach ($currentRowDimensions as $objRowDimension) {
            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
        }

        $this->rowDimensions = $newRowDimensions;

        return $this;
    }

    public function calculateWorksheetDimension()
    {
        return 'A1' . ':' . $this->getHighestColumn() . $this->getHighestRow();
    }

    public function calculateWorksheetDataDimension()
    {
        return 'A1' . ':' . $this->getHighestDataColumn() . $this->getHighestDataRow();
    }

    public function calculateColumnWidths()
    {
        $autoSizes = [];
        foreach ($this->getColumnDimensions() as $colDimension) {
            if ($colDimension->getAutoSize()) {
                $autoSizes[$colDimension->getColumnIndex()] = -1;
            }
        }

        if (!empty($autoSizes)) {
            $isMergeCell = [];
            foreach ($this->getMergeCells() as $cells) {
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
                    $isMergeCell[$cellReference] = true;
                }
            }

            foreach ($this->getCoordinates(false) as $coordinate) {
                $cell = $this->getCell($coordinate, false);
                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
                    //Determine if cell is in merge range
                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);

                    //By default merged cells should be ignored
                    $isMergedButProceed = false;

                    //The only exception is if it's a merge range value cell of a 'vertical' randge (1 column wide)
                    if ($isMerged && $cell->isMergeRangeValueCell()) {
                        $range = $cell->getMergeRange();
                        $rangeBoundaries = Coordinate::rangeDimension($range);
                        if ($rangeBoundaries[0] == 1) {
                            $isMergedButProceed = true;
                        }
                    }

                    if (!$isMerged || $isMergedButProceed) {
                        $cellValue = NumberFormat::toFormattedString(
                            $cell->getCalculatedValue(),
                            $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode()
                        );

                        $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
                            (float) $autoSizes[$this->cellCollection->getCurrentColumn()],
                            (float) Shared\Font::calculateColumnWidth(
                                $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(),
                                $cellValue,
                                $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getAlignment()->getTextRotation(),
                                $this->getParent()->getDefaultStyle()->getFont()
                            )
                        );
                    }
                }
            }

            foreach ($autoSizes as $columnIndex => $width) {
                if ($width == -1) {
                    $width = $this->getDefaultColumnDimension()->getWidth();
                }
                $this->getColumnDimension($columnIndex)->setWidth($width);
            }
        }

        return $this;
    }

    public function getParent()
    {
        return $this->parent;
    }

    public function rebindParent(Spreadsheet $parent)
    {
        if ($this->parent !== null) {
            $namedRanges = $this->parent->getNamedRanges();
            foreach ($namedRanges as $namedRange) {
                $parent->addNamedRange($namedRange);
            }

            $this->parent->removeSheetByIndex(
                $this->parent->getIndex($this)
            );
        }
        $this->parent = $parent;

        return $this;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function setTitle($pValue, $updateFormulaCellReferences = true, $validate = true)
    {
        if ($this->getTitle() == $pValue) {
            return $this;
        }

        $oldTitle = $this->getTitle();

        if ($validate) {
            self::checkSheetTitle($pValue);

            if ($this->parent) {
                if ($this->parent->sheetNameExists($pValue)) {

                    if (Shared\StringHelper::countCharacters($pValue) > 29) {
                        $pValue = Shared\StringHelper::substring($pValue, 0, 29);
                    }
                    $i = 1;
                    while ($this->parent->sheetNameExists($pValue . ' ' . $i)) {
                        ++$i;
                        if ($i == 10) {
                            if (Shared\StringHelper::countCharacters($pValue) > 28) {
                                $pValue = Shared\StringHelper::substring($pValue, 0, 28);
                            }
                        } elseif ($i == 100) {
                            if (Shared\StringHelper::countCharacters($pValue) > 27) {
                                $pValue = Shared\StringHelper::substring($pValue, 0, 27);
                            }
                        }
                    }

                    $pValue .= " $i";
                }
            }
        }

        $this->title = $pValue;
        $this->dirty = true;

        if ($this->parent && $this->parent->getCalculationEngine()) {
            $newTitle = $this->getTitle();
            $this->parent->getCalculationEngine()
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
            if ($updateFormulaCellReferences) {
                ReferenceHelper::getInstance()->updateNamedFormulas($this->parent, $oldTitle, $newTitle);
            }
        }

        return $this;
    }

    public function getSheetState()
    {
        return $this->sheetState;
    }

    public function setSheetState($value)
    {
        $this->sheetState = $value;

        return $this;
    }

    public function getPageSetup()
    {
        return $this->pageSetup;
    }

    public function setPageSetup(PageSetup $pValue)
    {
        $this->pageSetup = $pValue;

        return $this;
    }

    public function getPageMargins()
    {
        return $this->pageMargins;
    }

    public function setPageMargins(PageMargins $pValue)
    {
        $this->pageMargins = $pValue;

        return $this;
    }

    public function getHeaderFooter()
    {
        return $this->headerFooter;
    }

    public function setHeaderFooter(HeaderFooter $pValue)
    {
        $this->headerFooter = $pValue;

        return $this;
    }

    public function getSheetView()
    {
        return $this->sheetView;
    }

    public function setSheetView(SheetView $pValue)
    {
        $this->sheetView = $pValue;

        return $this;
    }

    public function getProtection()
    {
        return $this->protection;
    }

    public function setProtection(Protection $pValue)
    {
        $this->protection = $pValue;
        $this->dirty = true;

        return $this;
    }

    public function getHighestColumn($row = null)
    {
        if ($row == null) {
            return $this->cachedHighestColumn;
        }

        return $this->getHighestDataColumn($row);
    }

    public function getHighestDataColumn($row = null)
    {
        return $this->cellCollection->getHighestColumn($row);
    }

    public function getHighestRow($column = null)
    {
        if ($column == null) {
            return $this->cachedHighestRow;
        }

        return $this->getHighestDataRow($column);
    }

    public function getHighestDataRow($column = null)
    {
        return $this->cellCollection->getHighestRow($column);
    }

    public function getHighestRowAndColumn()
    {
        return $this->cellCollection->getHighestRowAndColumn();
    }

    public function setCellValue($pCoordinate, $pValue)
    {
        $this->getCell($pCoordinate)->setValue($pValue);

        return $this;
    }

    public function setCellValueByColumnAndRow($columnIndex, $row, $value)
    {
        $this->getCellByColumnAndRow($columnIndex, $row)->setValue($value);

        return $this;
    }

    public function setCellValueExplicit($pCoordinate, $pValue, $pDataType)
    {
        $this->getCell($pCoordinate)->setValueExplicit($pValue, $pDataType);

        return $this;
    }

    public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType)
    {
        $this->getCellByColumnAndRow($columnIndex, $row)->setValueExplicit($value, $dataType);

        return $this;
    }

    public function getCell($pCoordinate, $createIfNotExists = true)
    {
        if ($this->cellCollection->has(strtoupper($pCoordinate))) {
            return $this->cellCollection->get($pCoordinate);
        }

        if (strpos($pCoordinate, '!') !== false) {
            $worksheetReference = self::extractSheetTitle($pCoordinate, true);

            return $this->parent->getSheetByName($worksheetReference[0])->getCell(strtoupper($worksheetReference[1]), $createIfNotExists);
        }

        if ((!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate, $matches)) &&
            (preg_match('/^' . Calculation::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $pCoordinate, $matches))) {
            $namedRange = NamedRange::resolveRange($pCoordinate, $this);
            if ($namedRange !== null) {
                $pCoordinate = $namedRange->getRange();

                return $namedRange->getWorksheet()->getCell($pCoordinate, $createIfNotExists);
            }
        }

        $pCoordinate = strtoupper($pCoordinate);

        if (Coordinate::coordinateIsRange($pCoordinate)) {
            throw new Exception('Cell coordinate can not be a range of cells.');
        } elseif (strpos($pCoordinate, '$') !== false) {
            throw new Exception('Cell coordinate must not be absolute.');
        }

        return $createIfNotExists ? $this->createNewCell($pCoordinate) : null;
    }

    public function getCellByColumnAndRow($columnIndex, $row, $createIfNotExists = true)
    {
        $columnLetter = Coordinate::stringFromColumnIndex($columnIndex);
        $coordinate = $columnLetter . $row;

        if ($this->cellCollection->has($coordinate)) {
            return $this->cellCollection->get($coordinate);
        }

        return $createIfNotExists ? $this->createNewCell($coordinate) : null;
    }

    private function createNewCell($pCoordinate)
    {
        $cell = new Cell(null, DataType::TYPE_NULL, $this);
        $this->cellCollection->add($pCoordinate, $cell);
        $this->cellCollectionIsSorted = false;

        $aCoordinates = Coordinate::coordinateFromString($pCoordinate);
        if (Coordinate::columnIndexFromString($this->cachedHighestColumn) < Coordinate::columnIndexFromString($aCoordinates[0])) {
            $this->cachedHighestColumn = $aCoordinates[0];
        }
        $this->cachedHighestRow = max($this->cachedHighestRow, $aCoordinates[1]);

        $rowDimension = $this->getRowDimension($aCoordinates[1], false);
        $columnDimension = $this->getColumnDimension($aCoordinates[0], false);

        if ($rowDimension !== null && $rowDimension->getXfIndex() > 0) {
            $cell->setXfIndex($rowDimension->getXfIndex());
        } elseif ($columnDimension !== null && $columnDimension->getXfIndex() > 0) {
            $cell->setXfIndex($columnDimension->getXfIndex());
        }

        return $cell;
    }

    public function cellExists($pCoordinate)
    {
        if (strpos($pCoordinate, '!') !== false) {
            $worksheetReference = self::extractSheetTitle($pCoordinate, true);

            return $this->parent->getSheetByName($worksheetReference[0])->cellExists(strtoupper($worksheetReference[1]));
        }

        if ((!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate, $matches)) &&
            (preg_match('/^' . Calculation::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $pCoordinate, $matches))) {
            $namedRange = NamedRange::resolveRange($pCoordinate, $this);
            if ($namedRange !== null) {
                $pCoordinate = $namedRange->getRange();
                if ($this->getHashCode() != $namedRange->getWorksheet()->getHashCode()) {
                    if (!$namedRange->getLocalOnly()) {
                        return $namedRange->getWorksheet()->cellExists($pCoordinate);
                    }

                    throw new Exception('Named range ' . $namedRange->getName() . ' is not accessible from within sheet ' . $this->getTitle());
                }
            } else {
                return false;
            }
        }

        $pCoordinate = strtoupper($pCoordinate);

        if (Coordinate::coordinateIsRange($pCoordinate)) {
            throw new Exception('Cell coordinate can not be a range of cells.');
        } elseif (strpos($pCoordinate, '$') !== false) {
            throw new Exception('Cell coordinate must not be absolute.');
        }

        return $this->cellCollection->has($pCoordinate);
    }

    public function cellExistsByColumnAndRow($columnIndex, $row)
    {
        return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
    }

    public function getRowDimension($pRow, $create = true)
    {
        $found = null;

        if (!isset($this->rowDimensions[$pRow])) {
            if (!$create) {
                return null;
            }
            $this->rowDimensions[$pRow] = new RowDimension($pRow);

            $this->cachedHighestRow = max($this->cachedHighestRow, $pRow);
        }

        return $this->rowDimensions[$pRow];
    }

    public function getColumnDimension($pColumn, $create = true)
    {
        $pColumn = strtoupper($pColumn);

        if (!isset($this->columnDimensions[$pColumn])) {
            if (!$create) {
                return null;
            }
            $this->columnDimensions[$pColumn] = new ColumnDimension($pColumn);

            if (Coordinate::columnIndexFromString($this->cachedHighestColumn) < Coordinate::columnIndexFromString($pColumn)) {
                $this->cachedHighestColumn = $pColumn;
            }
        }

        return $this->columnDimensions[$pColumn];
    }

    public function getColumnDimensionByColumn($columnIndex)
    {
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
    }

    public function getStyles()
    {
        return $this->styles;
    }

    public function getStyle($pCellCoordinate)
    {
        $this->parent->setActiveSheetIndex($this->parent->getIndex($this));

        $this->setSelectedCells(strtoupper($pCellCoordinate));

        return $this->parent->getCellXfSupervisor();
    }

    public function getConditionalStyles($pCoordinate)
    {
        $pCoordinate = strtoupper($pCoordinate);
        if (!isset($this->conditionalStylesCollection[$pCoordinate])) {
            $this->conditionalStylesCollection[$pCoordinate] = [];
        }

        return $this->conditionalStylesCollection[$pCoordinate];
    }

    public function conditionalStylesExists($pCoordinate)
    {
        return isset($this->conditionalStylesCollection[strtoupper($pCoordinate)]);
    }

    public function removeConditionalStyles($pCoordinate)
    {
        unset($this->conditionalStylesCollection[strtoupper($pCoordinate)]);

        return $this;
    }

    public function getConditionalStylesCollection()
    {
        return $this->conditionalStylesCollection;
    }

    public function setConditionalStyles($pCoordinate, $pValue)
    {
        $this->conditionalStylesCollection[strtoupper($pCoordinate)] = $pValue;

        return $this;
    }

    public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
    {
        if ($columnIndex2 !== null && $row2 !== null) {
            $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;

            return $this->getStyle($cellRange);
        }

        return $this->getStyle(Coordinate::stringFromColumnIndex($columnIndex1) . $row1);
    }

    public function duplicateStyle(Style $pCellStyle, $pRange)
    {
        $workbook = $this->parent;
        if ($existingStyle = $this->parent->getCellXfByHashCode($pCellStyle->getHashCode())) {
            $xfIndex = $existingStyle->getIndex();
        } else {
            $workbook->addCellXf($pCellStyle);
            $xfIndex = $pCellStyle->getIndex();
        }

        list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($pRange . ':' . $pRange);

        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
            $tmp = $rangeStart;
            $rangeStart = $rangeEnd;
            $rangeEnd = $tmp;
        }

        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
            }
        }

        return $this;
    }

    public function duplicateConditionalStyle(array $pCellStyle, $pRange = '')
    {
        foreach ($pCellStyle as $cellStyle) {
            if (!($cellStyle instanceof Conditional)) {
                throw new Exception('Style is not a conditional style');
            }
        }

        list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($pRange . ':' . $pRange);

        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
            $tmp = $rangeStart;
            $rangeStart = $rangeEnd;
            $rangeEnd = $tmp;
        }

        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $pCellStyle);
            }
        }

        return $this;
    }

    public function setBreak($pCoordinate, $pBreak)
    {
        $pCoordinate = strtoupper($pCoordinate);

        if ($pCoordinate != '') {
            if ($pBreak == self::BREAK_NONE) {
                if (isset($this->breaks[$pCoordinate])) {
                    unset($this->breaks[$pCoordinate]);
                }
            } else {
                $this->breaks[$pCoordinate] = $pBreak;
            }
        } else {
            throw new Exception('No cell coordinate specified.');
        }

        return $this;
    }

    public function setBreakByColumnAndRow($columnIndex, $row, $break)
    {
        return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break);
    }

    public function getBreaks()
    {
        return $this->breaks;
    }

    public function mergeCells($pRange)
    {
        $pRange = strtoupper($pRange);

        if (strpos($pRange, ':') !== false) {
            $this->mergeCells[$pRange] = $pRange;


            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);

            $upperLeft = $aReferences[0];
            if (!$this->cellExists($upperLeft)) {
                $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
            }

            $count = count($aReferences);
            for ($i = 1; $i < $count; ++$i) {
                if ($this->cellExists($aReferences[$i])) {
                    $this->getCell($aReferences[$i])->setValueExplicit(null, DataType::TYPE_NULL);
                }
            }
        } else {
            throw new Exception('Merge must be set on a range of cells.');
        }

        return $this;
    }

    public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
    {
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;

        return $this->mergeCells($cellRange);
    }

    public function unmergeCells($pRange)
    {
        $pRange = strtoupper($pRange);

        if (strpos($pRange, ':') !== false) {
            if (isset($this->mergeCells[$pRange])) {
                unset($this->mergeCells[$pRange]);
            } else {
                throw new Exception('Cell range ' . $pRange . ' not known as merged.');
            }
        } else {
            throw new Exception('Merge can only be removed from a range of cells.');
        }

        return $this;
    }

    public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
    {
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;

        return $this->unmergeCells($cellRange);
    }

    public function getMergeCells()
    {
        return $this->mergeCells;
    }

    public function setMergeCells(array $pValue)
    {
        $this->mergeCells = $pValue;

        return $this;
    }

    public function protectCells($pRange, $pPassword, $pAlreadyHashed = false)
    {
        $pRange = strtoupper($pRange);

        if (!$pAlreadyHashed) {
            $pPassword = Shared\PasswordHasher::hashPassword($pPassword);
        }
        $this->protectedCells[$pRange] = $pPassword;

        return $this;
    }

    public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false)
    {
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;

        return $this->protectCells($cellRange, $password, $alreadyHashed);
    }

    public function unprotectCells($pRange)
    {
        $pRange = strtoupper($pRange);

        if (isset($this->protectedCells[$pRange])) {
            unset($this->protectedCells[$pRange]);
        } else {
            throw new Exception('Cell range ' . $pRange . ' not known as protected.');
        }

        return $this;
    }

    public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
    {
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;

        return $this->unprotectCells($cellRange);
    }

    public function getProtectedCells()
    {
        return $this->protectedCells;
    }

    public function getAutoFilter()
    {
        return $this->autoFilter;
    }

    public function setAutoFilter($pValue)
    {
        if (is_string($pValue)) {
            $this->autoFilter->setRange($pValue);
        } elseif (is_object($pValue) && ($pValue instanceof AutoFilter)) {
            $this->autoFilter = $pValue;
        }

        return $this;
    }

    public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
    {
        return $this->setAutoFilter(
            Coordinate::stringFromColumnIndex($columnIndex1) . $row1
            . ':' .
            Coordinate::stringFromColumnIndex($columnIndex2) . $row2
        );
    }

    public function removeAutoFilter()
    {
        $this->autoFilter->setRange(null);

        return $this;
    }

    public function getFreezePane()
    {
        return $this->freezePane;
    }

    public function freezePane($cell, $topLeftCell = null)
    {
        if (is_string($cell) && Coordinate::coordinateIsRange($cell)) {
            throw new Exception('Freeze pane can not be set on a range of cells.');
        }

        if ($cell !== null && $topLeftCell === null) {
            $coordinate = Coordinate::coordinateFromString($cell);
            $topLeftCell = $coordinate[0] . $coordinate[1];
        }

        $this->freezePane = $cell;
        $this->topLeftCell = $topLeftCell;

        return $this;
    }

    public function freezePaneByColumnAndRow($columnIndex, $row)
    {
        return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row);
    }

    public function unfreezePane()
    {
        return $this->freezePane(null);
    }

    public function getTopLeftCell()
    {
        return $this->topLeftCell;
    }

    public function insertNewRowBefore($pBefore, $pNumRows = 1)
    {
        if ($pBefore >= 1) {
            $objReferenceHelper = ReferenceHelper::getInstance();
            $objReferenceHelper->insertNewBefore('A' . $pBefore, 0, $pNumRows, $this);
        } else {
            throw new Exception('Rows can only be inserted before at least row 1.');
        }

        return $this;
    }

    public function insertNewColumnBefore($pBefore, $pNumCols = 1)
    {
        if (!is_numeric($pBefore)) {
            $objReferenceHelper = ReferenceHelper::getInstance();
            $objReferenceHelper->insertNewBefore($pBefore . '1', $pNumCols, 0, $this);
        } else {
            throw new Exception('Column references should not be numeric.');
        }

        return $this;
    }

    public function insertNewColumnBeforeByIndex($beforeColumnIndex, $pNumCols = 1)
    {
        if ($beforeColumnIndex >= 1) {
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $pNumCols);
        }

        throw new Exception('Columns can only be inserted before at least column A (1).');
    }

    public function removeRow($pRow, $pNumRows = 1)
    {
        if ($pRow >= 1) {
            $highestRow = $this->getHighestDataRow();
            $objReferenceHelper = ReferenceHelper::getInstance();
            $objReferenceHelper->insertNewBefore('A' . ($pRow + $pNumRows), 0, -$pNumRows, $this);
            for ($r = 0; $r < $pNumRows; ++$r) {
                $this->getCellCollection()->removeRow($highestRow);
                --$highestRow;
            }
        } else {
            throw new Exception('Rows to be deleted should at least start from row 1.');
        }

        return $this;
    }

    public function removeColumn($pColumn, $pNumCols = 1)
    {
        if (!is_numeric($pColumn)) {
            $highestColumn = $this->getHighestDataColumn();
            $pColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($pColumn) + $pNumCols);
            $objReferenceHelper = ReferenceHelper::getInstance();
            $objReferenceHelper->insertNewBefore($pColumn . '1', -$pNumCols, 0, $this);
            for ($c = 0; $c < $pNumCols; ++$c) {
                $this->getCellCollection()->removeColumn($highestColumn);
                $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
            }
        } else {
            throw new Exception('Column references should not be numeric.');
        }

        return $this;
    }

    public function removeColumnByIndex($columnIndex, $numColumns = 1)
    {
        if ($columnIndex >= 1) {
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
        }

        throw new Exception('Columns to be deleted should at least start from column A (1)');
    }

    public function getShowGridlines()
    {
        return $this->showGridlines;
    }

    public function setShowGridlines($pValue)
    {
        $this->showGridlines = $pValue;

        return $this;
    }

    public function getPrintGridlines()
    {
        return $this->printGridlines;
    }

    public function setPrintGridlines($pValue)
    {
        $this->printGridlines = $pValue;

        return $this;
    }

    public function getShowRowColHeaders()
    {
        return $this->showRowColHeaders;
    }

    public function setShowRowColHeaders($pValue)
    {
        $this->showRowColHeaders = $pValue;

        return $this;
    }

    public function getShowSummaryBelow()
    {
        return $this->showSummaryBelow;
    }

    public function setShowSummaryBelow($pValue)
    {
        $this->showSummaryBelow = $pValue;

        return $this;
    }

    public function getShowSummaryRight()
    {
        return $this->showSummaryRight;
    }

    public function setShowSummaryRight($pValue)
    {
        $this->showSummaryRight = $pValue;

        return $this;
    }

    public function getComments()
    {
        return $this->comments;
    }

    public function setComments(array $pValue)
    {
        $this->comments = $pValue;

        return $this;
    }

    public function getComment($pCellCoordinate)
    {
        $pCellCoordinate = strtoupper($pCellCoordinate);

        if (Coordinate::coordinateIsRange($pCellCoordinate)) {
            throw new Exception('Cell coordinate string can not be a range of cells.');
        } elseif (strpos($pCellCoordinate, '$') !== false) {
            throw new Exception('Cell coordinate string must not be absolute.');
        } elseif ($pCellCoordinate == '') {
            throw new Exception('Cell coordinate can not be zero-length string.');
        }

        if (isset($this->comments[$pCellCoordinate])) {
            return $this->comments[$pCellCoordinate];
        }

        $newComment = new Comment();
        $this->comments[$pCellCoordinate] = $newComment;

        return $newComment;
    }

    public function getCommentByColumnAndRow($columnIndex, $row)
    {
        return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row);
    }

    public function getActiveCell()
    {
        return $this->activeCell;
    }

    public function getSelectedCells()
    {
        return $this->selectedCells;
    }

    public function setSelectedCell($pCoordinate)
    {
        return $this->setSelectedCells($pCoordinate);
    }

    public function setSelectedCells($pCoordinate)
    {
        $pCoordinate = strtoupper($pCoordinate);

        $pCoordinate = preg_replace('/^([A-Z]+)$/', '${1}:${1}', $pCoordinate);

        $pCoordinate = preg_replace('/^(\d+)$/', '${1}:${1}', $pCoordinate);

        $pCoordinate = preg_replace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $pCoordinate);

        $pCoordinate = preg_replace('/^(\d+):(\d+)$/', 'A${1}:XFD${2}', $pCoordinate);

        if (Coordinate::coordinateIsRange($pCoordinate)) {
            list($first) = Coordinate::splitRange($pCoordinate);
            $this->activeCell = $first[0];
        } else {
            $this->activeCell = $pCoordinate;
        }
        $this->selectedCells = $pCoordinate;

        return $this;
    }

    public function setSelectedCellByColumnAndRow($columnIndex, $row)
    {
        return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row);
    }

    public function getRightToLeft()
    {
        return $this->rightToLeft;
    }

    public function setRightToLeft($value)
    {
        $this->rightToLeft = $value;

        return $this;
    }

    public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false)
    {
        if (!is_array(end($source))) {
            $source = [$source];
        }

        list($startColumn, $startRow) = Coordinate::coordinateFromString($startCell);

        foreach ($source as $rowData) {
            $currentColumn = $startColumn;
            foreach ($rowData as $cellValue) {
                if ($strictNullComparison) {
                    if ($cellValue !== $nullValue) {
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
                    }
                } else {
                    if ($cellValue != $nullValue) {
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
                    }
                }
                ++$currentColumn;
            }
            ++$startRow;
        }

        return $this;
    }

    public function rangeToArray($pRange, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
    {
        $returnValue = [];
        list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($pRange);
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
        $minRow = $rangeStart[1];
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
        $maxRow = $rangeEnd[1];

        ++$maxCol;
        $r = -1;
        for ($row = $minRow; $row <= $maxRow; ++$row) {
            $rRef = ($returnCellRef) ? $row : ++$r;
            $c = -1;
            for ($col = $minCol; $col != $maxCol; ++$col) {
                $cRef = ($returnCellRef) ? $col : ++$c;
                if ($this->cellCollection->has($col . $row)) {
                    $cell = $this->cellCollection->get($col . $row);
                    if ($cell->getValue() !== null) {
                        if ($cell->getValue() instanceof RichText) {
                            $returnValue[$rRef][$cRef] = $cell->getValue()->getPlainText();
                        } else {
                            if ($calculateFormulas) {
                                $returnValue[$rRef][$cRef] = $cell->getCalculatedValue();
                            } else {
                                $returnValue[$rRef][$cRef] = $cell->getValue();
                            }
                        }

                        if ($formatData) {
                            $style = $this->parent->getCellXfByIndex($cell->getXfIndex());
                            $returnValue[$rRef][$cRef] = NumberFormat::toFormattedString(
                                $returnValue[$rRef][$cRef],
                                ($style && $style->getNumberFormat()) ? $style->getNumberFormat()->getFormatCode() : NumberFormat::FORMAT_GENERAL
                            );
                        }
                    } else {
                        $returnValue[$rRef][$cRef] = $nullValue;
                    }
                } else {
                    $returnValue[$rRef][$cRef] = $nullValue;
                }
            }
        }

        return $returnValue;
    }

    public function namedRangeToArray($pNamedRange, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
    {
        $namedRange = NamedRange::resolveRange($pNamedRange, $this);
        if ($namedRange !== null) {
            $pWorkSheet = $namedRange->getWorksheet();
            $pCellRange = $namedRange->getRange();

            return $pWorkSheet->rangeToArray($pCellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
        }

        throw new Exception('Named Range ' . $pNamedRange . ' does not exist.');
    }

    public function toArray($nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
    {
        $this->garbageCollect();

        $maxCol = $this->getHighestColumn();
        $maxRow = $this->getHighestRow();

        return $this->rangeToArray('A1:' . $maxCol . $maxRow, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
    }

    public function getRowIterator($startRow = 1, $endRow = null)
    {
        return new RowIterator($this, $startRow, $endRow);
    }

    public function getColumnIterator($startColumn = 'A', $endColumn = null)
    {
        return new ColumnIterator($this, $startColumn, $endColumn);
    }

    public function garbageCollect()
    {
        $this->cellCollection->get('A1');

        $colRow = $this->cellCollection->getHighestRowAndColumn();
        $highestRow = $colRow['row'];
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);

        foreach ($this->columnDimensions as $dimension) {
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
        }

        foreach ($this->rowDimensions as $dimension) {
            $highestRow = max($highestRow, $dimension->getRowIndex());
        }

        if ($highestColumn < 1) {
            $this->cachedHighestColumn = 'A';
        } else {
            $this->cachedHighestColumn = Coordinate::stringFromColumnIndex($highestColumn);
        }
        $this->cachedHighestRow = $highestRow;

        return $this;
    }

    public function getHashCode()
    {
        if ($this->dirty) {
            $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__);
            $this->dirty = false;
        }

        return $this->hash;
    }

    public static function extractSheetTitle($pRange, $returnRange = false)
    {
        if (($sep = strrpos($pRange, '!')) === false) {
            return $returnRange ? ['', $pRange] : '';
        }

        if ($returnRange) {
            return [substr($pRange, 0, $sep), substr($pRange, $sep + 1)];
        }

        return substr($pRange, $sep + 1);
    }

    public function getHyperlink($pCellCoordinate)
    {
        if (isset($this->hyperlinkCollection[$pCellCoordinate])) {
            return $this->hyperlinkCollection[$pCellCoordinate];
        }

        $this->hyperlinkCollection[$pCellCoordinate] = new Hyperlink();

        return $this->hyperlinkCollection[$pCellCoordinate];
    }

    public function setHyperlink($pCellCoordinate, Hyperlink $pHyperlink = null)
    {
        if ($pHyperlink === null) {
            unset($this->hyperlinkCollection[$pCellCoordinate]);
        } else {
            $this->hyperlinkCollection[$pCellCoordinate] = $pHyperlink;
        }

        return $this;
    }

    public function hyperlinkExists($pCoordinate)
    {
        return isset($this->hyperlinkCollection[$pCoordinate]);
    }

    public function getHyperlinkCollection()
    {
        return $this->hyperlinkCollection;
    }

    public function getDataValidation($pCellCoordinate)
    {
        if (isset($this->dataValidationCollection[$pCellCoordinate])) {
            return $this->dataValidationCollection[$pCellCoordinate];
        }

        $this->dataValidationCollection[$pCellCoordinate] = new DataValidation();

        return $this->dataValidationCollection[$pCellCoordinate];
    }

    public function setDataValidation($pCellCoordinate, DataValidation $pDataValidation = null)
    {
        if ($pDataValidation === null) {
            unset($this->dataValidationCollection[$pCellCoordinate]);
        } else {
            $this->dataValidationCollection[$pCellCoordinate] = $pDataValidation;
        }

        return $this;
    }

    public function dataValidationExists($pCoordinate)
    {
        return isset($this->dataValidationCollection[$pCoordinate]);
    }

    public function getDataValidationCollection()
    {
        return $this->dataValidationCollection;
    }

    public function shrinkRangeToFit($range)
    {
        $maxCol = $this->getHighestColumn();
        $maxRow = $this->getHighestRow();
        $maxCol = Coordinate::columnIndexFromString($maxCol);

        $rangeBlocks = explode(' ', $range);
        foreach ($rangeBlocks as &$rangeSet) {
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);

            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
            }
            if ($rangeBoundaries[0][1] > $maxRow) {
                $rangeBoundaries[0][1] = $maxRow;
            }
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
            }
            if ($rangeBoundaries[1][1] > $maxRow) {
                $rangeBoundaries[1][1] = $maxRow;
            }
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
        }
        unset($rangeSet);
        $stRange = implode(' ', $rangeBlocks);

        return $stRange;
    }

    public function getTabColor()
    {
        if ($this->tabColor === null) {
            $this->tabColor = new Color();
        }

        return $this->tabColor;
    }

    public function resetTabColor()
    {
        $this->tabColor = null;
        unset($this->tabColor);

        return $this;
    }

    public function isTabColorSet()
    {
        return $this->tabColor !== null;
    }

    public function copy()
    {
        $copied = clone $this;

        return $copied;
    }

    public function __clone()
    {
        foreach ($this as $key => $val) {
            if ($key == 'parent') {
                continue;
            }

            if (is_object($val) || (is_array($val))) {
                if ($key == 'cellCollection') {
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
                    $this->cellCollection = $newCollection;
                } elseif ($key == 'drawingCollection') {
                    $currentCollection = $this->drawingCollection;
                    $this->drawingCollection = new ArrayObject();
                    foreach ($currentCollection as $item) {
                        if (is_object($item)) {
                            $newDrawing = clone $item;
                            $newDrawing->setWorksheet($this);
                        }
                    }
                } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) {
                    $newAutoFilter = clone $this->autoFilter;
                    $this->autoFilter = $newAutoFilter;
                    $this->autoFilter->setParent($this);
                } else {
                    $this->{$key} = unserialize(serialize($val));
                }
            }
        }
    }

    public function setCodeName($pValue, $validate = true)
    {
        if ($this->getCodeName() == $pValue) {
            return $this;
        }

        if ($validate) {
            $pValue = str_replace(' ', '_', $pValue); //Excel does this automatically without flinching, we are doing the same

            self::checkSheetCodeName($pValue);


            if ($this->getParent()) {
                if ($this->getParent()->sheetCodeNameExists($pValue)) {

                    if (Shared\StringHelper::countCharacters($pValue) > 29) {
                        $pValue = Shared\StringHelper::substring($pValue, 0, 29);
                    }
                    $i = 1;
                    while ($this->getParent()->sheetCodeNameExists($pValue . '_' . $i)) {
                        ++$i;
                        if ($i == 10) {
                            if (Shared\StringHelper::countCharacters($pValue) > 28) {
                                $pValue = Shared\StringHelper::substring($pValue, 0, 28);
                            }
                        } elseif ($i == 100) {
                            if (Shared\StringHelper::countCharacters($pValue) > 27) {
                                $pValue = Shared\StringHelper::substring($pValue, 0, 27);
                            }
                        }
                    }

                    $pValue = $pValue . '_' . $i; // ok, we have a valid name
                }
            }
        }

        $this->codeName = $pValue;

        return $this;
    }

    public function getCodeName()
    {
        return $this->codeName;
    }

    public function hasCodeName()
    {
        return $this->codeName !== null;
    }
}

